#extracts the path from certain opinion to the base opinion from a list of opinions
from sent.model.entities.opinion import Opinion, OpinionEvaluations,\
    OpinionState
from sent.utils.attention import Deltas
from sent.model.OpinionDBManager import OpinionDBManager
from sent.utils.update_range import UpdateRagesSet, UpdateRange
import sys
from sent.utils import CommonResult, ErrorIds
import time
from uuid import UUID
def extract_path_to_base(opinion, opinionsList):
    result = []
    current_op = opinion
    assert isinstance(current_op, Opinion)
    while current_op.parent_id != current_op.id:
        is_parent_found = False
        for op in opinionsList:
            if op.id == current_op.parent_id:
                result.append(op)
                current_op = op
                is_parent_found = True
        if not is_parent_found:
            return False, []
        
    return True, result

def change_att_deltas_to_base(deltas, path_to_base, positive, negative):
    assert isinstance(deltas, Deltas)
    prev_op = None
    
    for op in path_to_base:
        deltas.change_delta(op.id, positive, negative)
        if op.evaluation == OpinionEvaluations.NEUTRAL:
            break
        elif op.evaluation == OpinionEvaluations.NEGATIVE:
            #swap negative and positive
            buf = positive
            positive = negative
            negative = buf 
        if prev_op and prev_op.parent_id != op.id:
            raise AssertionError
        prev_op = op
        
def neutralize_non_neutral_opinion(non_neutral_op_from_this_usr, opinions_to_update, deltas, db_man):
    impact = {}
    if non_neutral_op_from_this_usr:
        assert isinstance(non_neutral_op_from_this_usr, Opinion)
        pathFound, path_to_non_neutral_op = extract_path_to_base(non_neutral_op_from_this_usr, opinions_to_update)
        assert pathFound
        
        if non_neutral_op_from_this_usr.evaluation == OpinionEvaluations.POSITIVE:
            change_att_deltas_to_base(deltas, path_to_non_neutral_op, 
                                        -1 - non_neutral_op_from_this_usr.positive_attention, 
                                        -non_neutral_op_from_this_usr.negative_attention)
        elif non_neutral_op_from_this_usr.evaluation == OpinionEvaluations.NEGATIVE:
            change_att_deltas_to_base(deltas, path_to_non_neutral_op,
                                        -non_neutral_op_from_this_usr.negative_attention, 
                                        -1 - non_neutral_op_from_this_usr.positive_attention)
        
        impact['modified_opinion'] = {}
        impact['modified_opinion']['obj'] = non_neutral_op_from_this_usr.toPrimitiveDict()
        
        if non_neutral_op_from_this_usr.has_content:
            
            if non_neutral_op_from_this_usr.op_state == OpinionState.NORMAL:
                non_neutral_op_from_this_usr.op_state = OpinionState.WAITING_UPDATE
                non_neutral_op_from_this_usr.prev_eval = non_neutral_op_from_this_usr.evaluation
            
            impact['modified_opinion']['modification'] = 'neutralization'
            
            non_neutral_op_from_this_usr.evaluation = OpinionEvaluations.NEUTRAL
        
#            db_man.update_parent_stats(non_neutral_op_from_this_usr.parent_id, 0,
#                                    -(non_neutral_op_from_this_usr.prev_eval == OpinionEvaluations.POSITIVE),
#                                    1, 
#                                    -(non_neutral_op_from_this_usr.prev_eval == OpinionEvaluations.NEGATIVE))
            
            
            
            db_man.update_opinion(non_neutral_op_from_this_usr)
        else:
            #assert non_neutral_op_from_this_usr.op_state==OpinionState.INSERTED \
            #    or non_neutral_op_from_this_usr.nested_set_right - non_neutral_op_from_this_usr.nested_set_left == 1
            
            impact['modified_opinion']['modification'] = 'deletion'
            
            db_man.delete_opinion(non_neutral_op_from_this_usr, True)
            
        
    return impact

def process_nested_set_pending_operations(output, commit_delay):
    dbman = OpinionDBManager(False)
    cmn_result = CommonResult()
    cmn_result.result = {}
    try:
        pending_ud, opinions_to_process = dbman.get_opinions_for_insert_delete()
        
        if pending_ud and len(opinions_to_process) > 0:
            opinions_to_delete = [item for item in opinions_to_process if item.op_state == OpinionState.WAITING_DELETE]
            opinions_to_create = [item for item in opinions_to_process if item.op_state == OpinionState.INSERTED]
            opinions_to_update = [item for item in opinions_to_process if item.op_state == OpinionState.WAITING_UPDATE]
            
            assert(len(opinions_to_delete) + len(opinions_to_create) + len(opinions_to_update) == len(opinions_to_process))
            
            parents_of_created_list = dbman.get_batch_opinions_by_ids(set([op.parent_id for op in opinions_to_create]))
            
            parents_of_created = dict((op.id, op) for op in parents_of_created_list)
            
            def filter_parents_of_inserted(deleted, created):
                for del_el in deleted:
                    found_child=False
                    for ins_el in created:
                        if del_el.isParentOrSameAs(parents_of_created[ins_el.parent_id]):
                            print >> output, "Weird: created opinion inside deleted one (%s) (%s)" % (ins_el, del_el) 
                            
                            found_child=True
                            del_el.prev_eval = del_el.evaluation
                            del_el.evaluation = OpinionEvaluations.NEUTRAL
                            #del_el.op_state = OpinionState.WAITING_UPDATE
                                
                            opinions_to_update.append(del_el)
                            #dbman.update_opinion(del_el)
                            break
                    if not found_child:
                        yield del_el
            
            opinions_to_delete = list(filter_parents_of_inserted(opinions_to_delete, opinions_to_create))
            
            deltas = Deltas()
            
            for op_del1 in opinions_to_delete:
                path_to_op = dbman.get_path_to_opinions_safe(True, op_del1)
                
                #TODO may not be necessary
                pathFound, path_to_op = extract_path_to_base(op_del1, path_to_op)
                assert pathFound
                
                #Neutralize op_del1
                if op_del1.evaluation == OpinionEvaluations.POSITIVE:
                    change_att_deltas_to_base(deltas, path_to_op, -op_del1.positive_attention - 1, -op_del1.negative_attention)
                elif op_del1.evaluation == OpinionEvaluations.NEGATIVE:
                    change_att_deltas_to_base(deltas, path_to_op, -op_del1.negative_attention, -op_del1.positive_attention - 1)
            
            for op_upd in opinions_to_update:
                path_to_op = dbman.get_path_to_opinions_safe(True, op_upd)
                
                pathFound, path_to_op = extract_path_to_base(op_upd, path_to_op)
                assert pathFound
                
                #Neutralize op_upd
                if op_upd.prev_eval == OpinionEvaluations.POSITIVE:
                    change_att_deltas_to_base(deltas, path_to_op, -op_upd.positive_attention - 1, -op_upd.negative_attention)
                elif op_upd.prev_eval == OpinionEvaluations.NEGATIVE:
                    change_att_deltas_to_base(deltas, path_to_op, -op_upd.negative_attention, -op_upd.positive_attention - 1)
                
                #Now apply the new evaluation (if any)
                if op_upd.evaluation == OpinionEvaluations.POSITIVE:
                    change_att_deltas_to_base(deltas, path_to_op, op_upd.positive_attention + 1, op_upd.negative_attention)
                elif op_upd.evaluation == OpinionEvaluations.NEGATIVE:
                    change_att_deltas_to_base(deltas, path_to_op, op_upd.negative_attention, op_upd.positive_attention + 1)
            
            for op_cr in opinions_to_create:
                prent_op = parents_of_created[op_cr.parent_id]
                path_to_parent = dbman.get_path_to_opinions(True, prent_op)
                
                #TODO may not be necessary
                pathFound, path_to_op = extract_path_to_base(prent_op, path_to_parent)
                assert pathFound
                
                path_to_op.insert(0, prent_op)
                
                if op_cr.evaluation == OpinionEvaluations.POSITIVE:
                    change_att_deltas_to_base(deltas, path_to_op, 1, 0)
                elif op_cr.evaluation == OpinionEvaluations.NEGATIVE:
                    change_att_deltas_to_base(deltas, path_to_op, 0, 1)
            
            update_ranges = UpdateRagesSet()
            
            for op_del in opinions_to_delete:
                assert isinstance(op_del, Opinion)
                if op_del.has_content:
                    update_ranges.merge_range(UpdateRange(op_del.nested_set_right + 1, sys.maxint, -2))
            
            for op_create in opinions_to_create:
                paren = parents_of_created[op_create.parent_id]
                if op_create.has_content:
                    update_ranges.merge_range(UpdateRange(paren.nested_set_right, sys.maxint, 2))
            
            dbman.batch_delete_opinions([del_op.id for del_op in opinions_to_delete])
            
            for op_upd in opinions_to_update:
                op_upd.prev_eval = OpinionEvaluations.NEUTRAL
                op_upd.op_state = OpinionState.NORMAL
                dbman.update_opinion(op_upd)
            
            deltas_list = [delta for delta in deltas.getAllDeltas()]
            dbman.apply_attention_deltas(deltas_list)
            
            dbman.apply_update_ranges_set(update_ranges, opinions_to_process[0].base_op_id)
            
            for op in parents_of_created_list:
                dbman.expunge(op)
            
            
#            to cut unnecessary data in NPP
#            \t*("nested_set_.*)
#            --\1
#            
#            ^\t+\"[^\{\}\r\n]+$
            
            #apply the offsets to the parents of the created opinions so we know where to insert the opinions
            for parent in parents_of_created_list:
                lft_idx = update_ranges.find_range_idx(parent.nested_set_left)
                assert lft_idx >= 0
                
                rght_idx = update_ranges.find_range_idx(parent.nested_set_right, lft_idx)
                assert rght_idx >= 0
                
                parent.nested_set_left += update_ranges.ranges[lft_idx].offset
                parent.nested_set_right += update_ranges.ranges[rght_idx].offset
                
            for op_create in opinions_to_create:
                if op_create.has_content:
                    parent_op = parents_of_created[op_create.parent_id]
                    assert parent_op.nested_set_right > parent_op.nested_set_left + 2
                    
                    #due to it is likely that several new opinions are added to same parent, here I
                    #decrement the parent_op.right so that the next opinions receive correct left and right values as well
                    #note: this ways the order of the opinions added by single call to process_nested_set_pending_operations
                    #is unpredictable. But that's not so huge problem since few seconds difference is not a big deal anyways
                    #TODO maybe fix this
                    parent_op.nested_set_right = parent_op.nested_set_right - 2
                    op_create.nested_set_left = parent_op.nested_set_right
                    op_create.nested_set_right = op_create.nested_set_left + 1
                    
                op_create.op_state = OpinionState.NORMAL
                dbman.update_opinion(op_create)
            
            cmn_result.errorId = ErrorIds.SUCCESS
            uuid_str = str(UUID(bytes=pending_ud.id))
            cmn_result.result['base_id'] = uuid_str
            cmn_result.result['deleted'] = len(opinions_to_delete)
            cmn_result.result['created'] = len(opinions_to_create)
            cmn_result.result['updated'] = len(opinions_to_update)
            
            dbman.pending_updates_deletes_finished(pending_ud)
            
            if commit_delay>0:
                time.sleep(commit_delay)
            
            dbman.commitSession()
        else:
            if pending_ud:
                dbman.pending_updates_deletes_finished(pending_ud)
                dbman.commitSession()
                cmn_result.errorId = ErrorIds.SUCCESS
            else:
                cmn_result.errorId = ErrorIds.NOT_FOUND
    finally:
        dbman.closeSession()
        
    return cmn_result